/*
* v4l.cc Video4Linux management
* Copyright (C) 2001 Charles Yates <charles.yates@pandora.be>
* Copyright (C) 2001-2007 Dan Dennedy <dan@dennedy.org>
* Copyright (C) 2007 Stéphane Brunner <stephane.brunner@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <deque>
#include <unistd.h>
#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

using std::ostringstream;
using std::setw;
using std::setfill;

#include <pthread.h>

#include "v4l.h"

extern "C"
{
#include <sys/types.h>
#include <sys/soundcard.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

	pthread_t v4lthread;
}

static int ENCODE_YUV = 0;

/** Send a request to the v4l device associated to a V4LStruct object.
*/

bool V4LDevice::request( int req, V4LStruct *v4l )
{
	ENCODE_YUV = getenv( "KINO_V4L_FFMPEG" ) != NULL;
	return request( req, v4l->getStruct() );
}

/** Send a request to the v4l device associated to an arbitrary address.
*/

bool V4LDevice::request( int req, void *addr )
{
	return ioctl( getHandle(), req, addr ) != -1;
}

V4LCapability::V4LCapability( V4LDevice *device )
{
	device->request( VIDIOCGCAP, this );
}

V4LCapability::~V4LCapability()
{
}

void *V4LCapability::getStruct()
{
	return & capability;
}

char *V4LCapability::getName()
{
	return capability.name;
}

int V4LCapability::getNumberOfChannels()
{
	return capability.channels;
}

int V4LCapability::getNumberOfAudioDevices()
{
	return capability.audios;
}

int V4LCapability::getMinWidth()
{
	return capability.minwidth;
}

int V4LCapability::getMinHeight()
{
	return capability.minheight;
}

int V4LCapability::getMaxWidth()
{
	return capability.maxwidth;
}

int V4LCapability::getMaxHeight()
{
	return capability.maxheight;
}

bool V4LCapability::canCapture()
{
	return capability.type & VID_TYPE_CAPTURE;
}

bool V4LCapability::hasTuner()
{
	return capability.type & VID_TYPE_TUNER;
}

bool V4LCapability::hasChromakey()
{
	return capability.type & VID_TYPE_CHROMAKEY;
}

bool V4LCapability::hasClipping()
{
	return capability.type & VID_TYPE_CLIPPING;
}

bool V4LCapability::hasOverwrite()
{
	return capability.type & VID_TYPE_FRAMERAM;
}

bool V4LCapability::hasScaling()
{
	return capability.type & VID_TYPE_SCALES;
}

bool V4LCapability::isMonochrome()
{
	return capability.type & VID_TYPE_MONOCHROME;
}

bool V4LCapability::canSubCapture()
{
	return capability.type & VID_TYPE_SUBCAPTURE;
}

V4LTuner::V4LTuner( V4LDevice *device, int index )
{
	this->device = device;
	this->tuner.tuner = index;
	this->device->request( VIDIOCGTUNER, this );
}

void *V4LTuner::getStruct()
{
	return & tuner;
}

int V4LTuner::getRangeLow()
{
	return tuner.rangelow;
}

void V4LTuner::setRangeLow( int low )
{
	tuner.rangelow = low;
}

int V4LTuner::getRangeHigh()
{
	return tuner.rangehigh;
}

void V4LTuner::setRangeHigh( int high )
{
	tuner.rangehigh = high;
}

int V4LTuner::getFlags()
{
	return tuner.flags;
}

void V4LTuner::setFlags( int flags )
{
	tuner.flags = flags;
}

int V4LTuner::getMode()
{
	return tuner.mode;
}

void V4LTuner::setMode( int mode )
{
	tuner.mode = mode;
}

int V4LTuner::getSignal()
{
	return tuner.signal;
}

V4LChannel::V4LChannel( V4LDevice *device, int index )
{
	memset( &channel, 0, sizeof( struct video_channel ) );
	this->device = device;
	this->channel.channel = index;
	device->request( VIDIOCGCHAN, this );
	device->request( VIDIOCSCHAN, this );
	for ( unsigned int i = 0; i < getNumberOfTuners(); i ++ )
	{
		V4LTuner *tuner = new V4LTuner( this->device, i );
		tuners.insert( tuners.end(), tuner );
	}
}

V4LChannel::~V4LChannel()
{
}

void *V4LChannel::getStruct()
{
	return & channel;
}

char *V4LChannel::getName()
{
	return channel.name;
}

bool V4LChannel::setTuner( unsigned int index )
{
	if ( index >= 0 && index < tuners.size() )
	{
		current = tuners[ index ];
		// FIXME: Hardcoded tuner settings
		current->setRangeLow( 0 );
		current->setRangeHigh( 0xffff );
		return device->request( VIDIOCSTUNER, current );
	}
	else
	{
		return false;
	}
}

unsigned int V4LChannel::getNumberOfTuners()
{
	return channel.tuners;
}

V4LTuner *V4LChannel::getTuner( unsigned int index )
{
	if ( index >= 0 && index < tuners.size() )
	{
		return tuners[ index ];
	}
	else
	{
		return NULL;
	}
}

int V4LChannel::getSignal()
{
	device->request( VIDIOCGTUNER, current );
	return current->getSignal();
}

/** Constructor for the V4L class.
*/

V4L::V4L()
{
}

void V4L::setInfo( char *device, char *input, char *audio, int sample )
{
	this->device = device;
	this->input = input;
	this->audio = audio;
	this->sample = sample;
}

bool V4L::openDevice()
{
	bool ret = true;

	if ( !strcmp( this->input, "PAL" ) )
	{
		this->width = 720;
		this->height = 576;
		this->fps = 25;
		this->frameSample = this->sample / this->fps;
	}
	else if ( !strcmp( this->input, "NTSC" ) )
	{
		this->width = 720;
		this->height = 480;
		this->fps = 30;
		this->frameSample = this->sample / this->fps;
	}

	this->current = NULL;
	this->fd = open( device, O_RDWR );
	if ( fd != -1 )
	{
		this->capability = new V4LCapability( this );
		for ( int index = 0; index < capability->getNumberOfChannels(); index ++ )
		{
			V4LChannel *channel = new V4LChannel( this, index );
			channels.insert( channels.end(), channel );
		}
		setCaptureResolution( this->width, this->height );
	}
	else
	{
		perror( "Unable to open video device" );
		ret = false;
	}
	return ret;
}

/** Destructor for the V4L device.
*/

V4L::~V4L()
{
	if ( fd != -1 )
	{
		close( fd );
		for ( unsigned int index = 0; index < channels.size(); index ++ )
			delete channels[ index ];
		delete this->capability;
	}
}

/** Indicate if the device is available.
 
  	\return true if available, false otherwise
*/

bool V4L::deviceAvailable()
{
	return fd != -1;
}

/** Return the handle associated to the device.
 
  	\return the file descriptor of the open device.
*/

int V4L::getHandle()
{
	return fd;
}

/** Set specified channel.
 
  	\param channel		channel to use
	\return true if successful, false otherwise
*/

bool V4L::setChannel( unsigned int channel )
{
	if ( channel >= 0 && channel < channels.size() )
	{
		current = channels[ channel ];
		return this->request( VIDIOCSCHAN, current );
	}
	else
	{
		return false;
	}
}

/**	Get the number of channels available.
 
  	\return the number of channels
*/

unsigned int V4L::getNumberOfChannels()
{
	return channels.size();
}

/**	Get the specified channel.
 
  	\param	channel 	channel to obtain
  	\return the number of channels
*/

V4LChannel *V4L::getChannel( unsigned int channel )
{
	if ( channel >= 0 && channel < channels.size() )
		return channels[ channel ];
	else
		return NULL;
}

/** Set specified tuner.
 
	\param	tuner		tuner to use
	\return true if successful, false otherwise
*/

bool V4L::setTuner( unsigned int tuner )
{
	if ( current != NULL )
		return current->setTuner( tuner );
	else
		return false;
}

/** Get the number of tuners associated to the current channel.
 
  	\return	the number of tuners associated to the current channel
*/

unsigned int V4L::getNumberOfTuners( )
{
	if ( current != NULL )
		return current->getNumberOfTuners();
	else
		return 0;
}

/** Get a tuner associated to the current channel.
 
  	\param	tuner	tuner object to obtain
	\return a tuner object 
*/

V4LTuner *V4L::getTuner( unsigned int tuner )
{
	if ( current != NULL )
		return current->getTuner( tuner );
	else
		return NULL;
}

/** Set user defined resolution (where applicable).
 
  	\param	width		width of capture
	\param	height		height of capture
*/

bool V4L::setCaptureResolution( int width, int height )
{
	if ( width > capability->getMaxWidth() ||
	        width < capability->getMinWidth() )
		return false;
	if ( height > capability->getMaxHeight() ||
	        height < capability->getMinHeight() )
		return false;
	if ( !capability->hasScaling() && (
	            width != capability->getMaxWidth() ||
	            height != capability->getMaxHeight() ) )
		return false;
	this->width = width;
	this->height = height;
	return true;
}

/** Get the capture width.
 
  	\return	the width of the captured image
*/

int V4L::getWidth()
{
	return width;
}

/** Get the capture height.
 
  	\return	the height of the captured image
*/

int V4L::getHeight()
{
	return height;
}

/** Turn on audio.
*/

void V4L::startAudio()
{
	struct video_audio audio;
	ioctl( fd, VIDIOCGAUDIO, &audio );
	if ( audio.flags & VIDEO_AUDIO_MUTE )
		audio.flags ^= VIDEO_AUDIO_MUTE;
	audio.volume = 65535;
	ioctl( fd, VIDIOCSAUDIO, &audio );
}

/** Turn off audio.
*/

int V4L::mappedMemorySize( bool init )
{
	static video_mbuf buf;
	if ( init == true )
	{
		init = 1;
		ioctl( fd, VIDIOCGMBUF, &buf );
		frame_maps = buf.frames;
	}
	return buf.size;
}

/** Initialise capture.
 
  	\param	format	v4l frame format (VIDEO_PALETTE_)
	\return true if successful, false otherwise
*/

bool V4L::initialiseCapture( int format )
{
	size = width * height * 4;

	map = mmap( 0, mappedMemorySize( true ), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );

	if ( map != NULL )
	{
		for ( int i = 0; i < frame_maps; i ++ )
		{
			frame[ i ].frame = i;
			frame[ i ].width = getWidth();
			frame[ i ].height = getHeight();
			frame[ i ].format = format;
		}
		
		struct timeval tv;
		gettimeofday( &tv, NULL );
		starttime = tv.tv_sec * 1000000 + tv.tv_usec;
		frames = 0;
		frame_next = 0;
		
		int retry = 0;
		while ( ioctl( fd, VIDIOCMCAPTURE, &frame[ 0 ] ) == -1 && retry ++ < frame_maps + 1 ) ;
		
		return true;
	}
	else
	{
		return false;
	}
}

/** Get the next frame.
 
  	\return	the adress of the frame in the format specified
*/

void *V4L::getNextFrame()
{
	unsigned char * ret = NULL;

	int current = frame_next;
	frame_next = ( frame_next + 1 ) % frame_maps;
	
	if ( ioctl( fd, VIDIOCMCAPTURE, &frame[ frame_next ] ) == -1 )
		; //cout << "Frame 1 Failed to initialise" << endl;
	if ( ioctl( fd, VIDIOCSYNC, &frame[ current ].frame ) == -1 )
		; //cout << "Frame 0 Failed to sync" << endl;
	ret = ( unsigned char * ) map + current * ( mappedMemorySize( ) / frame_maps );
	
	frames ++;

	return ( void * ) ret;
}

/** Turn off capture.
*/

void V4L::stopCapture()
{
	if ( map != NULL )
	{
		struct timeval tv;
		gettimeofday( &tv, NULL );
		long long endtime = tv.tv_sec * 1000000 + tv.tv_usec;
		double fps = ( frames ) / ( ( ( double ) ( endtime - starttime ) ) / 1000000 );
		munmap( map, mappedMemorySize() );
		map = NULL;
		int enable = 0;
		ioctl( getHandle(), VIDIOCCAPTURE, &enable );
	}
}

/** Get the current frequency of the tuner.
 
  	\return	the current tuned in frequency.
*/

int V4L::getFrequency()
{
	unsigned long current;
	ioctl( fd, VIDIOCGFREQ, &current );
	return ( int ) current;
}

/** Set the current frequency of the tuner.
 
  	\param	frequency	frequency to set
  	\return	the current tuned in frequency.
*/

bool V4L::setFrequency( int frequency )
{
	unsigned long val = ( unsigned long ) frequency & 0xffff;
	return ioctl( fd, VIDIOCSFREQ, &val ) != -1;
}

/** Get the signal of the current tuned in frequency.
*/

int V4L::getSignal()
{
	return current->getSignal();
}


/**
 * Rudimentary encoding class for raw dv frame
 */

EncoderFrame::EncoderFrame()
{
	image = new uint8_t[ 720 * 576 * 3 ];
}

EncoderFrame::~EncoderFrame()
{
	delete[] image;
}

void EncoderFrame::setVideo( void *image, int width, int height )
{
	if ( ENCODE_YUV )
		memcpy( this->image, ( unsigned char * ) image, width * height * 2 );
	else
		memcpy( this->image, ( unsigned char * ) image, ( width * height * 3 ) / 2 );

	this->width = width;
	this->height = height;
}

/**
 * Rudimentary encoding class for raw dv stream
 */

deque < EncoderFrame * > DvEncoder::used;
deque < EncoderFrame * > DvEncoder::available;

DvEncoder::DvEncoder( char *filename, int width, int height ) : active( false ), audio( "/dev/dsp" )
{
	this->width = width;
	this->height = height;

	this->filename = filename;
	for ( int i = 0; i < 50; ++i )
	{
		available.push_back( NULL );
	}

	pthread_mutex_init( &mutex, NULL );
	pthread_create( &thread, NULL, startThread, this );
}

DvEncoder::~DvEncoder( )
{
	active = false;
	pthread_join( thread, NULL );

	for ( int i = available.size(); i > 0; --i )
	{
		EncoderFrame *frame = available[ 0 ];
		available.pop_front();
		delete frame;
	}

	for ( int i = used.size(); i > 0; --i )
	{
		EncoderFrame *frame = used[ 0 ];
		used.pop_front();
		delete frame;
	}
}

EncoderFrame *DvEncoder::getFrame()
{
	EncoderFrame * frame = NULL;

	pthread_mutex_lock( &mutex );
	if ( available.size() > 0 )
	{
		frame = available[ 0 ];
		if ( frame == NULL )
			frame = new EncoderFrame;

		available.pop_front();
	}
	pthread_mutex_unlock( &mutex );

	return frame;
}

void DvEncoder::doneWithFrame( EncoderFrame *frame )
{
	pthread_mutex_lock( &mutex );
	used.push_back( frame );
	pthread_mutex_unlock( &mutex );
}

void *DvEncoder::startThread( void *ptr )
{
	DvEncoder * encoder = ( DvEncoder * ) ptr;
	encoder->writeThread( );
	return NULL;
}

void DvEncoder::writeThread()
{
	struct timespec tm = { 0, 1000000 }; // 1ms
	char command[ 10240 ];
	int counter = 0;
	struct stat stats;
//	string thisfile;

	active = true;

//	do
//	{
//		ostringstream sb;
//		sb << filename << setfill( '0' ) << setw( 3 ) << ++ counter << ".dv";
//		thisfile = sb.str();
//	}
//	while ( stat( thisfile.c_str(), &stats ) == 0 );

//	KinoVideoPipe *encode = NULL;

//	if ( ENCODE_YUV )
//	{
////		encode = KinoVideoFactory::CreateVideoPipe( PIPE_VIDEO_DV_YUV );
//		sprintf( command, "ffmpeg -threads 2 -f yuv4mpegpipe -i pipe: -f audio_device -ac 2 -ar 48000 -i %s -y %s", audio, thisfile.c_str() );
////		sprintf( command, "ffmpeg -f yuv4mpegpipe -i pipe: -f audio_device -ac 2 -ar 48000 -i %s -vcodec mpeg4 -qscale 1 -aspect 4:3 -acodec pcm_s16le -y %s.avi", audio, thisfile.c_str() );
//	}
//	else
//	{
////		encode = KinoVideoFactory::CreateVideoPipe( PIPE_VIDEO_DV_PGM );
//		sprintf( command, "encodedv -a dsp -i pgm -p 2 -q 2 - %s > %s", audio, thisfile.c_str() );
//	}

//	encode->OpenVideoPipe( command, width, height );

	while ( active || used.size() > 0 )
	{
		if ( used.size() > 0 )
		{
			pthread_mutex_lock( &mutex );
			EncoderFrame *frame = used.front();
			used.pop_front();
			pthread_mutex_unlock( &mutex );

			if ( frame != NULL )
			{
				pthread_mutex_lock( &mutex );
				available.push_back( frame );
				pthread_mutex_unlock( &mutex );
			}
		}

		nanosleep( &tm, NULL );
	}

//	FileTracker::GetInstance().Add( thisfile.c_str() );
}

/**
 * Rudimentary V4l Display and Capture class
 */

extern "C"
{
	static void *gdkv4l_thread( void * ptr )
	{
		GDKV4L * v4l = ( GDKV4L * ) ptr;
		v4l->active = true;
		if ( !v4l->capturing )
		{
			while ( v4l->active )
			{
				v4l->draw();
				struct timespec t = { 0, 1000000 }; // 1ms
				nanosleep( &t, NULL );
			}
		}
		else
		{
			while ( v4l->active )
			{
				struct timespec t = { 0, 1000000 };
				nanosleep( &t, NULL );
			}
		}
		return NULL;
	}
}

GDKV4L::GDKV4L() : V4L(), active( false ), capturing( false )
{
	this->displayer = NULL;
}

GDKV4L::~GDKV4L()
{
	stopCapturing();
	stopVideo();
}

void GDKV4L::startVideo()
{
	if ( !active )
	{
		pthread_create( &v4lthread, NULL, gdkv4l_thread, this );
	}
}

void GDKV4L::startCapturing()
{
	if ( !capturing )
	{
		stopVideo();
		capturing = true;
		startVideo();
	}
}

void GDKV4L::stopVideo()
{
	if ( active )
	{
		active = false;
//		gdk_threads_leave();
		pthread_join( v4lthread, NULL );
//		gdk_threads_enter();
		this->stopCapture();
	}
	if ( displayer != NULL )
	{
		delete displayer;
		displayer = NULL;
	}
}

void GDKV4L::stopCapturing()
{
	if ( capturing )
	{
		stopVideo();
		capturing = false;
		startVideo();
	}
}

void GDKV4L::draw()
{

/*	if ( displayer == NULL )
	{
		gdk_threads_enter();
		displayer = FindDisplayer::getDisplayer( widget, getWidth(), getHeight() );

		switch ( displayer->format() )
		{
		case DISPLAY_YUV:
			input = DISPLAY_YUV;
			initialiseCapture( VIDEO_PALETTE_YUV422 );
			break;
		case DISPLAY_RGB:
			input = DISPLAY_BGR;
			initialiseCapture( VIDEO_PALETTE_RGB24 );
			break;
		case DISPLAY_RGB16:
			input = DISPLAY_RGB16;
			initialiseCapture( VIDEO_PALETTE_RGB565 );
			break;
		default:
			break;
		}
		gdk_threads_leave();
	}*/

	void *f = getNextFrame();

	if ( f != NULL )
	{
//		gdk_threads_enter();
//		displayer->put( input, f, getWidth(), getHeight() );
//		displayer->put( f, getWidth(), getHeight() );
//		gdk_flush();
//		gdk_threads_leave();
	}
}

void GDKV4L::capture( DvEncoder *encoder )
{

	static int count = 0;
	void *f = NULL;

/*	if ( displayer == NULL )
	{
		gdk_threads_enter();
		displayer = FindDisplayer::getDisplayer( widget, getWidth(), getHeight() );

		if ( ENCODE_YUV )
		{
			input = DISPLAY_YUV;	// Fake - YUV422P isn't supported by Displayer
			initialiseCapture( VIDEO_PALETTE_YUV422P );
		}
		else
		{
			input = DISPLAY_RGB;	// Fake - YUV420P isn't supported by Displayer
			initialiseCapture( VIDEO_PALETTE_YUV420P );
		}

		// skip the first frame
		f = getNextFrame();

		gdk_threads_leave();
	}*/


	f = getNextFrame();

	if ( f != NULL )
	{
		EncoderFrame * frame = encoder->getFrame();
		if ( frame != NULL )
		{
			frame->setVideo( f, getWidth(), getHeight() );
			encoder->doneWithFrame( frame );
		}
	}
}

